/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.ui;

import java.awt.Font;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;

import gjexer.TApplication;
import gjexer.TEditColorThemeWindow;
import gjexer.TExceptionDialog;
import gjexer.TFileOpenBox;
import gjexer.TInputBox;
import gjexer.TMessageBox;
import gjexer.TStatusBar;
import gjexer.TWindow;
import gjexer.backend.Backend;
import gjexer.backend.SwingTerminal;
import gjexer.bits.CellAttributes;
import gjexer.event.TCommandEvent;
import gjexer.event.TKeypressEvent;
import gjexer.event.TMenuEvent;
import gjexer.menu.TMenu;
import gjexer.menu.TSubMenu;
import static gjexer.TCommand.*;
import static gjexer.TKeypress.*;

import tjide.build.CompileListener;
import tjide.debugger.Breakpoint;
import tjide.debugger.Debugger;
import tjide.debugger.DebuggerListener;
import tjide.debugger.Scope;
import tjide.project.FileTarget;
import tjide.project.JavaTarget;
import tjide.project.NotRunnableException;
import tjide.project.Project;
import tjide.project.RunnableTarget;
import tjide.project.Target;

/**
 * The TJIDE application itself.
 */
public class TranquilApplication extends TApplication
                                 implements DebuggerListener {

    /**
     * Translated strings.
     */
    private static ResourceBundle i18n = ResourceBundle.getBundle(TranquilApplication.class.getName());

    // ------------------------------------------------------------------------
    // Constants --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Release version.
     */
    public static final String VERSION = "1.0\u03b2";

    /*
     * Available menu commands.
     */

    // Package private so that InternalEditor can see it.
    static final int MENU_FILE_SAVE                     = 2020;
    // Package private so that InternalEditor can see it.
    static final int MENU_FILE_SAVE_AS                  = 2021;

    private static final int MENU_FILE_SAVE_ALL         = 2022;

    private static final int MENU_SEARCH_PREVIOUS_ERROR = 2044;
    private static final int MENU_SEARCH_NEXT_ERROR     = 2045;

    /**
     * The menu ID associated with Run | Run.
     */
    public static final int MENU_RUN_RUN                = 2050;

    private static final int MENU_RUN_PROGRAM_RESET     = 2051;
    private static final int MENU_RUN_GO_TO_CURSOR      = 2052;
    private static final int MENU_RUN_TRACE_INTO        = 2053;
    private static final int MENU_RUN_STEP_OVER         = 2054;
    // Package private so that InternalEditor can see it.
    static final int MENU_RUN_ARGUMENTS                 = 2055;


    private static final int MENU_COMPILE_COMPILE       = 2060;

    /**
     * The menu ID associated with Compile | Make.
     */
    public static final int MENU_COMPILE_MAKE           = 2061;

    /**
     * The menu ID associated with Compile | Build.
     */
    public static final int MENU_COMPILE_BUILD_ALL      = 2062;

    private static final int MENU_COMPILE_REM_MESSAGES  = 2063;

    private static final int MENU_DEBUG_LOCALS          = 2070;
    private static final int MENU_DEBUG_CALLSTACK       = 2071;
    // Package private so that InternalEditor can see it.
    static final int MENU_DEBUG_TOGGLE_BREAKPOINT       = 2072;
    static final int MENU_DEBUG_BREAKPOINTS             = 2073;

    private static final int MENU_PROJECT_NEW           = 2080;
    private static final int MENU_PROJECT_OPEN          = 2081;
    private static final int MENU_PROJECT_CLOSE         = 2082;
    private static final int MENU_PROJECT_ADD_ITEM      = 2083;
    private static final int MENU_PROJECT_DELETE_ITEM   = 2084;
    private static final int MENU_PROJECT_LOCAL_OPTIONS = 2085;

    private static final int MENU_OPTIONS_PROJECT       = 2090;
    private static final int MENU_OPTIONS_APPLICATION   = 2091;
    private static final int MENU_OPTIONS_COLORS        = 2092;
    private static final int MENU_OPTIONS_EDITOR        = 2093;
    private static final int MENU_OPTIONS_INTERFACE     = 2094;
    private static final int MENU_OPTIONS_LOAD          = 2098;
    private static final int MENU_OPTIONS_SAVE          = 2099;

    private static final int MENU_WINDOW_COMPILE        = 2101;
    private static final int MENU_WINDOW_MESSAGE        = 2102;
    private static final int MENU_WINDOW_OUTPUT         = 2103;
    private static final int MENU_WINDOW_WATCH          = 2104;
    private static final int MENU_WINDOW_PROJECT        = 2105;
    private static final int MENU_WINDOW_PROJECT_NOTES  = 2106;
    private static final int MENU_WINDOW_LIST_ALL       = 2107;

    private static final int MENU_WATCH_ADD             = 2110;
    private static final int MENU_WATCH_DELETE          = 2112;
    private static final int MENU_WATCH_EDIT            = 2113;
    private static final int MENU_WATCH_REMOVE_ALL      = 2114;

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The compile status window shows the current progress of the compile.
     */
    private CompileStatusWindow compileStatusWindow;

    /**
     * The message window shows previous messages from the compile.
     */
    private MessageWindow messageWindow;

    /**
     * The watch window shows the watches.
     */
    private WatchWindow watchWindow;

    /**
     * The locals window shows the local variables.
     */
    private LocalsWindow localsWindow;

    /**
     * The callstack window shows the call stack.
     */
    private CallstackWindow callstackWindow;

    /**
     * The project window shows the targets in the project.
     */
    private ProjectWindow projectWindow;

    /**
     * The project being worked on.
     */
    private Project project;

    /**
     * The global options.
     */
    private Properties options = null;

    /**
     * The filename to save options to.
     */
    private String configFilename = null;

    /**
     * If true, use the external editor.  Note package private access.
     */
    boolean useExternalEditor = false;

    /**
     * Set of open targets and their associated internal editor windows.
     */
    private Map<FileTarget, TargetEditor> editingTargets =
                                      new HashMap<FileTarget, TargetEditor>();

    /**
     * The interface to the running debugger, or null if nothing is being
     * run.
     */
    private Debugger debugger = null;

    /**
     * The last seen scope in the running debugger, or null if nothing has
     * been run.
     */
    private Scope debugScope = null;

    /**
     * The currently-debugged program output window.
     */
    private TWindow outputWindow;

    /**
     * Last used search text.  Note package private access.
     */
    String searchText = "";

    /**
     * Last used search case sensitivity flag.  Note package private access.
     */
    boolean searchCaseSensitive = false;

    /**
     * Last used search regular expression flag.  Note package private
     * access.
     */
    boolean searchRegularExpression = false;

    /**
     * Last used search whole words only flag.  Note package private access.
     */
    boolean searchWholeWordsOnly = false;

    /**
     * Last used search direction.  1 means forward, 2 means backward.  Note
     * package private access.
     */
    int searchDirection = 1;

    /**
     * Last used search scope.  1 means entire file, 2 means selected text
     * only.  Note package private access.
     */
    int searchScope = 1;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Public constructor.
     *
     * @param backendType one of the TApplication.BackendType values
     * @throws Exception if TApplication can't instantiate the Backend.
     */
    public TranquilApplication(final BackendType backendType) throws Exception {
        super(backendType);

        TranquilApplicationImpl();
    }

    /**
     * Public constructor.
     *
     * @param backendType one of the TApplication.BackendType values
     * @param minimumWidth minimum width of window
     * @param minimumHeight minimum height of window
     * @param fontSize the size in points
     * @throws Exception if TApplication can't instantiate the Backend.
     */
    public TranquilApplication(final BackendType backendType,
        final int minimumWidth, final int minimumHeight,
        final int fontSize) throws Exception {

        super(backendType, Math.max(80, minimumWidth),
            Math.max(25, minimumHeight), fontSize);

        TranquilApplicationImpl();
    }

    /**
     * Public constructor.
     *
     * @param backend a Backend that is already ready to go.
     */
    public TranquilApplication(final Backend backend) {
        super(backend);

        TranquilApplicationImpl();
    }

    /**
     * Actual constructor logic.
     */
    private void TranquilApplicationImpl() {
        // We set some default colors here, so that saveOptions() can see
        // them.
        setTranquilColors();
        initializeOptions();
        addAllWidgets();
        getBackend().setTitle(i18n.getString("frameTitle"));
    }

    /**
     * Set Tranquil colors into the color theme.
     */
    private void setTranquilColors() {
        getTheme().setColorFromString("compileStatusWindow.labels",
            "black on white");
        getTheme().setColorFromString("compileStatusWindow.statusLabel",
            "cyan on blue");
        getTheme().setColorFromString("compileStatusWindow.statusText",
            "blink cyan on blue");
        getTheme().setColorFromString("messageWindow.background",
            "bold white on cyan");
        getTheme().setColorFromString("messageWindow.borderControls",
            "bold green on cyan");
        getTheme().setColorFromString("messageWindow.windowMove",
            "bold green on cyan");
        getTheme().setColorFromString("messageWindow.warning",
            "bold yellow on cyan");
        getTheme().setColorFromString("messageWindow.warning.selected",
            "bold yellow on blue");
        getTheme().setColorFromString("messageWindow.error",
            "red on cyan");
        getTheme().setColorFromString("messageWindow.error.selected",
            "red on blue");
        getTheme().setColorFromString("messageWindow.info",
            "bold white on cyan");
        getTheme().setColorFromString("messageWindow.info.selected",
            "bold white on blue");
        getTheme().setColorFromString("watchWindow.background",
            "bold white on cyan");
        getTheme().setColorFromString("watchWindow.borderControls",
            "bold green on cyan");
        getTheme().setColorFromString("watchWindow.windowMove",
            "bold green on cyan");
        getTheme().setColorFromString("watchWindow.item",
            "blue on cyan");
        getTheme().setColorFromString("watchWindow.item.selected",
            "bold cyan on green");
        getTheme().setColorFromString("localsWindow.background",
            "bold white on cyan");
        getTheme().setColorFromString("localsWindow.borderControls",
            "bold green on cyan");
        getTheme().setColorFromString("localsWindow.windowMove",
            "bold green on cyan");
        getTheme().setColorFromString("callstackWindow.background",
            "bold white on white");
        getTheme().setColorFromString("callstackWindow.borderControls",
            "bold green on white");
        getTheme().setColorFromString("callstackWindow.windowMove",
            "bold green on white");
        getTheme().setColorFromString("projectWindow.background",
            "bold white on cyan");
        getTheme().setColorFromString("projectWindow.borderControls",
            "bold green on cyan");
        getTheme().setColorFromString("projectWindow.windowMove",
            "bold green on cyan");
        getTheme().setColorFromString("teditor.breakpointLine",
            "bold white on red");
        getTheme().setColorFromString("teditor.executionLine",
            "black on cyan");
    }

    // ------------------------------------------------------------------------
    // TApplication behavior --------------------------------------------------
    // ------------------------------------------------------------------------


    // ------------------------------------------------------------------------
    // Event handlers ---------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Handle the File | New menu item.
     */
    private void menuFileNew() {
        new InternalEditorWindow(this, project);
    }

    /**
     * Handle the File | Open menu item.
     */
    private void menuFileOpen() {
        try {
            String filename = fileOpenBox(".");
            if (filename != null) {
                openEditor(filename);
            }
        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
        }
    }

    /**
     * Handle the File | Save all menu item.
     */
    private void menuFileSaveAll() {
        for (TargetEditor editor: editingTargets.values()) {
            if (editor instanceof InternalEditorWindow) {
                ((InternalEditorWindow) editor).saveIfDirty();
            }
        }
    }

    /**
     * Handle the Search | Find menu item.
     */
    private void menuSearchFind() {
        new SearchInputWindow(this, false);
    }

    /**
     * Handle the Search | Replace menu item.
     */
    private void menuSearchReplace() {
        new SearchInputWindow(this, true);
    }

    /**
     * Handle the Search | Search again menu item.
     */
    private void menuSearchAgain() {
        if (searchText.length() == 0) {
            return;
        }
        List<InternalEditorWindow> editors = getInternalEditors();

        if (editors.size() == 0) {
            return;
        }

        // Sort by Z, the first one should be the top-level editor.
        Collections.sort(editors);
        InternalEditorWindow editor = editors.get(0);
        assert (editor.getZ() == 0);

        int line = editor.getEditingRowNumber();
        line++;
        if (line > editor.getLineCount()) {
            line = 1;
        }
        SearchInputWindow.searchEditor(editor, searchText,
            (searchDirection == 1 ? true : false),
            line, (searchScope == 2 ? true : false),
            searchCaseSensitive, searchRegularExpression, searchWholeWordsOnly);
    }

    /**
     * Handle the Search | Go to line menu item.
     */
    private void menuSearchGoToLine() {
        TWindow window = getActiveWindow();
        if (window instanceof InternalEditorWindow) {
            TInputBox inputBox = inputBox(i18n.
                getString("searchGoToLineInputBoxTitle"),
                i18n.getString("searchGoToLineInputBoxCaption"), "",
                TInputBox.Type.OKCANCEL);

            if (!inputBox.isOk()) {
                return;
            }

            InternalEditorWindow editor = (InternalEditorWindow) window;
            if (inputBox.getText().trim().length() > 0) {
                int lineNumber = editor.getEditingRowNumber();
                try {
                    lineNumber = Integer.parseInt(inputBox.getText().trim());
                    if (lineNumber < 0) {
                        editor.setEditingRowNumber(1);
                    } else if (lineNumber > editor.getLineCount()) {
                        editor.setEditingRowNumber(editor.getLineCount());
                    } else {
                        editor.setEditingRowNumber(lineNumber);
                    }
                } catch (NumberFormatException e) {
                    // SQUASH
                }
            }
        }
    }

    /**
     * Handle the Run | Run menu item.
     */
    private void menuRunRun() {
        if (debugger != null) {
            // We are in a debugging session, resume.
            debugger.resume();
            return;
        }

        if (project != null) {
            RunnableTarget runnableTarget = null;

            for (Target target: project.getTargets()) {
                if (target instanceof RunnableTarget) {
                    if (((RunnableTarget) target).isRunnable()) {
                        if (target.getWindow() == getActiveWindow()) {
                            // If the active window is an editor window that
                            // is runnable, then run it.
                            try {
                                ((RunnableTarget) target).runTarget(this, project);
                            } catch (NotRunnableException e) {
                                // This is a programming error.
                                throw new IllegalStateException(e);
                            }
                            return;
                        }
                        if (runnableTarget != null) {
                            // The project has multiple runnable targets, Ask
                            // the user which target to run.
                            new RunTargetWindow(this, project);
                            return;
                        }
                        runnableTarget = (RunnableTarget) target;
                    }
                }
            }

            if (runnableTarget == null) {
                // No runnables in this project, bail out.
                return;
            }

            // The project has a single runnable target, run it.
            try {
                runnableTarget.runTarget(this, project);
            } catch (NotRunnableException e) {
                // This is a programming error.
                throw new IllegalStateException(e);
            }
        }
    }

    /**
     * Handle the Run | Program reset menu item.
     */
    private void menuRunProgramReset() {
        if (debugger != null) {
            debugger.reset();
            debugger = null;
        }
        debugScope = null;
        for (TargetEditor targetEditor: editingTargets.values()) {
            if (targetEditor instanceof InternalEditorWindow) {
                InternalEditorWindow editor;
                editor = (InternalEditorWindow) targetEditor;
                editor.setExecutionLine(-1);
            }
        }

        disableMenuItem(MENU_RUN_PROGRAM_RESET);
        disableMenuItem(MENU_DEBUG_CALLSTACK);
        disableMenuItem(MENU_DEBUG_LOCALS);

        // Update debug windows
        watchWindow.updateWatches();
        localsWindow.updateLocals();
        callstackWindow.updateCallstack();
    }

    /**
     * Handle the Run | Go to cursor menu item.
     */
    private void menuRunGoToCursor() {
        TWindow window = getActiveWindow();
        if (!(window instanceof InternalEditorWindow)) {
            return;
        }

        InternalEditorWindow editor = (InternalEditorWindow) window;
        FileTarget fileTarget = editor.getTarget();
        if (fileTarget == null) {
            return;
        }
        if (!(fileTarget instanceof RunnableTarget)) {
            return;
        }

        int line = editor.getEditingRowNumber();

        if (debugger == null) {
            // Debugger isn't running, spin it up and run to one location.
            fileTarget.runToLocation(line);
            try {
                ((RunnableTarget) fileTarget).runTarget(this, project);
            } catch (NotRunnableException e) {
                // This is a programming error.
                throw new IllegalStateException(e);
            }
            return;
        }
        // Debugger is already running, run to location.
        debugger.runToLocation(fileTarget, line);
    }

    /**
     * Handle the Run | Trace into cursor menu item.
     */
    private void menuRunTraceInto() {
        if (debugger == null) {
            return;
        }
        debugger.traceInto();
    }

    /**
     * Handle the Run | Step over cursor menu item.
     */
    private void menuRunStepOver() {
        if (debugger == null) {
            return;
        }
        debugger.stepOver();
    }

    /**
     * Handle the Debug | Toggle breakpoint menu item.
     */
    private void menuDebugToggleBreakpoint() {
        TWindow window = getActiveWindow();
        if (!(window instanceof InternalEditorWindow)) {
            return;
        }

        InternalEditorWindow editor = (InternalEditorWindow) window;
        FileTarget fileTarget = editor.getTarget();
        if (fileTarget == null) {
            return;
        }
        int line = editor.getEditingRowNumber();

        // If this breakpoint is real, remove it.
        for (Breakpoint breakpoint: fileTarget.getBreakpoints()) {
            if (breakpoint.getLine() == line) {
                fileTarget.removeBreakpoint(line);
                if (debugger != null) {
                    debugger.removeBreakpoint(fileTarget, line);
                }
                editor.removeBreakpoint(line);
                return;
            }
        }

        // The breakpoint needs to be added.
        fileTarget.addBreakpoint(line);
        if (debugger != null) {
            debugger.addBreakpoint(fileTarget, line);
        }
        editor.addBreakpoint(line);
    }

    /**
     * Handle the Project | New menu item.
     */
    private void menuProjectNew() {
        if (project != null) {
            if (messageBox(i18n.getString("closeProjectTitle"),
                    i18n.getString("closeProjectCaption"),
                    TMessageBox.Type.YESNO).isNo()) {
                return;
            }
            closeProject();
        }

        TInputBox inputBox = inputBox(i18n.getString("newProjectInputBoxTitle"),
            i18n.getString("newProjectInputBoxCaption"), "",
            TInputBox.Type.OKCANCEL);

        if (inputBox.getResult() == TInputBox.Result.OK) {
            String projectFilename = inputBox.getText().trim();
            if (!projectFilename.endsWith(".project")) {
                projectFilename += ".project";
            }
            project = new Project(this, new File(projectFilename), "", "", "");
            projectWindow = new ProjectWindow(this, project);
            new ProjectOptionsWindow(this, true);
            enableMenuItem(MENU_COMPILE_MAKE);
            enableMenuItem(MENU_COMPILE_BUILD_ALL);
            enableMenuItem(MENU_PROJECT_CLOSE);
            enableMenuItem(MENU_PROJECT_ADD_ITEM);
            enableMenuItem(MENU_PROJECT_DELETE_ITEM);
            enableMenuItem(MENU_PROJECT_LOCAL_OPTIONS);
            enableMenuItem(MENU_OPTIONS_PROJECT);
            enableMenuItem(MENU_WINDOW_PROJECT);
            enableMenuItem(MENU_WINDOW_PROJECT_NOTES);
        }
    }

    /**
     * Handle the Project | Open menu item.
     */
    private void menuProjectOpen() {
        try {
            String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
                "^.*\\.project$");
            if (filename != null) {
                if (filename.endsWith(".project")) {
                    openProject(filename);
                }
            }
        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
        }
    }

    /**
     * Handle menu events.
     *
     * @param menu menu event
     * @return if true, the event was processed and should not be passed onto
     * a window
     */
    @Override
    public boolean onMenu(final TMenuEvent menu) {

        // Dispatch menu event
        switch (menu.getId()) {

        case TMenu.MID_NEW:
            menuFileNew();
            return true;

        case TMenu.MID_OPEN_FILE:
            menuFileOpen();
            return true;

        case MENU_FILE_SAVE_ALL:
            menuFileSaveAll();
            return true;

        case TMenu.MID_EXIT:

            // Override MID_EXIT to show a different dialog.
            if (messageBox(i18n.getString("exitDialogTitle"),
                    i18n.getString("exitDialogText"),
                    TMessageBox.Type.YESNO).isYes()) {

                if (debugger != null) {
                    debugger.reset();
                    debugger = null;
                }
                debugScope = null;
                if (project != null) {
                    project.save();
                    closeProject();
                }
                exit();
            }
            return true;

        case TMenu.MID_CLOSE_ALL:
            // Override to only close non-permanent windows.
            closeNonpermanentWindows();
            return true;

        case TMenu.MID_FIND:
            menuSearchFind();
            return true;

        case TMenu.MID_REPLACE:
            menuSearchReplace();
            return true;

        case TMenu.MID_SEARCH_AGAIN:
            menuSearchAgain();
            return true;

        case TMenu.MID_GOTO_LINE:
            menuSearchGoToLine();
            return true;

        case MENU_SEARCH_PREVIOUS_ERROR:
            messageWindow.selectError(false);
            return true;

        case MENU_SEARCH_NEXT_ERROR:
            messageWindow.selectError(true);
            return true;

        case MENU_RUN_RUN:
            menuRunRun();
            return true;

        case MENU_RUN_PROGRAM_RESET:
            menuRunProgramReset();
            return true;

        case MENU_RUN_GO_TO_CURSOR:
            menuRunGoToCursor();
            return true;

        case MENU_RUN_TRACE_INTO:
            menuRunTraceInto();
            return true;

        case MENU_RUN_STEP_OVER:
            menuRunStepOver();
            return true;

        case MENU_RUN_ARGUMENTS:
            for (Target target: project.getTargets()) {
                if (target.getWindow() == getActiveWindow()) {
                    new TargetOptionsWindow(this, target);
                    break;
                }
            }
            return true;

        case MENU_COMPILE_COMPILE:
            if (project != null) {
                for (Target target: project.getTargets()) {
                    if (target.getWindow() == getActiveWindow()) {
                        // If the active window is an editor window, compile
                        // it.
                        target.compile(project);
                        return true;
                    }
                }
                // If the project window is on top, compile what is selected.
                if (projectWindow.isActive()) {
                    projectWindow.compileTarget();
                }
            }
            return true;

        case MENU_COMPILE_MAKE:
            if (project != null) {
                if (!compileStatusWindow.isShown()) {
                    project.make();
                }
            }
            return true;

        case MENU_COMPILE_BUILD_ALL:
            if (project != null) {
                if (!compileStatusWindow.isShown()) {
                    project.buildAll();
                }
            }
            return true;

        case MENU_COMPILE_REM_MESSAGES:
            messageWindow.clearMessages();
            return true;

        case MENU_DEBUG_LOCALS:
            localsWindow.show();
            localsWindow.activate();
            return true;

        case MENU_DEBUG_CALLSTACK:
            callstackWindow.show();
            callstackWindow.activate();
            return true;

        case MENU_DEBUG_BREAKPOINTS:
            new BreakpointsWindow(this);
            return true;

        case MENU_DEBUG_TOGGLE_BREAKPOINT:
            menuDebugToggleBreakpoint();
            return true;

        case MENU_WATCH_ADD:
            watchWindow.show();
            watchWindow.activate();
            watchWindow.addWatch();
            return true;

        case MENU_WATCH_DELETE:
            watchWindow.show();
            watchWindow.activate();
            watchWindow.deleteWatch();
            return true;

        case MENU_WATCH_EDIT:
            watchWindow.show();
            watchWindow.activate();
            watchWindow.editWatch();
            return true;

        case MENU_WATCH_REMOVE_ALL:
            watchWindow.show();
            watchWindow.activate();
            watchWindow.clearAll();
            return true;

        case MENU_PROJECT_NEW:
            menuProjectNew();
            return true;

        case MENU_PROJECT_OPEN:
            menuProjectOpen();
            return true;

        case MENU_PROJECT_CLOSE:
            closeProject();
            return true;

        case MENU_PROJECT_ADD_ITEM:
            if (projectWindow != null) {
                projectWindow.show();
                projectWindow.activate();
                new NewTargetWindow(projectWindow);
            }
            return true;

        case MENU_PROJECT_DELETE_ITEM:
            if (projectWindow != null) {
                if (projectWindow.isShown()) {
                    projectWindow.deleteTarget();
                }
            }
            return true;

        case MENU_PROJECT_LOCAL_OPTIONS:
            if (projectWindow != null) {
                if (projectWindow.isShown()) {
                    projectWindow.editTargetOptions();
                }
            }
            return true;

        case MENU_OPTIONS_PROJECT:
            // Display the project options window, it will be modal.
            if (project != null) {
                new ProjectOptionsWindow(this);
            }
            return true;

        case MENU_OPTIONS_APPLICATION:
            // Display the application options window, it will be modal.
            new ApplicationOptionsWindow(this);
            return true;

        case MENU_OPTIONS_COLORS:
            new TEditColorThemeWindow(this) {
                /*
                 * We have finished editing colors, now save to the
                 * tjide.properties file.
                 */
                @Override
                public void onClose() {
                    for (String key: getTheme().getColorNames()) {
                        options.setProperty("colors." + key,
                            getTheme().getColor(key).toString());
                    }
                    saveOptions();
                    super.onClose();
                }
            };
            return true;

        case MENU_OPTIONS_EDITOR:
            // Display the editor options window, it will be modal.
            new EditorOptionsWindow(this);
            return true;

        case MENU_OPTIONS_INTERFACE:
            // Display the Jexer options window, it will be modal.
            new JexerOptionsWindow(this);
            return true;

        case MENU_OPTIONS_LOAD:
            try {
                String filename = fileOpenBox(configFilename == null ?
                    System.getProperty("user.home") + "/.tjide" :
                    (new File(configFilename)).getParent());
                 if (filename != null) {
                     loadOptions(filename);
                 }
            } catch (IOException e) {
                // Show this exception to the user.
                new TExceptionDialog(this, e);
            }
            return true;

        case MENU_OPTIONS_SAVE:
            saveOptions();
            return true;

        case MENU_WINDOW_COMPILE:
            compileStatusWindow.show();
            return true;

        case MENU_WINDOW_MESSAGE:
            messageWindow.show();
            messageWindow.activate();
            return true;

        case MENU_WINDOW_WATCH:
            watchWindow.show();
            watchWindow.activate();
            return true;

        case MENU_WINDOW_OUTPUT:
            if (outputWindow != null) {
                outputWindow.show();
                outputWindow.activate();
            }
            return true;

        case MENU_WINDOW_PROJECT:
            if (projectWindow != null) {
                projectWindow.show();
                projectWindow.activate();
            }
            return true;

        case MENU_WINDOW_PROJECT_NOTES:
            if (project != null) {
                new ProjectNotesWindow(this);
            }
            return true;

        case MENU_WINDOW_LIST_ALL:
            new WindowListWindow(this);
            return true;

        default:
            // I did not handle it, pass it on.
            return super.onMenu(menu);
        }

    }

    /**
     * Method that TApplication subclasses can override to handle menu or
     * posted command events.
     *
     * @param command command event
     * @return if true, this event was consumed
     */
    @Override
    protected boolean onCommand(final TCommandEvent command) {
        // Override cmExit to show a different dialog.
        if (command.equals(cmExit)) {
            if (messageBox(i18n.getString("exitDialogTitle"),
                    i18n.getString("exitDialogText"),
                    TMessageBox.Type.YESNO).isYes()) {

                if (project != null) {
                    project.save();
                }
                if (debugger != null) {
                    debugger.reset();
                    debugger = null;
                }
                debugScope = null;
                exit();
            }
            return true;
        }

        // Override to only close non-permanent windows.
        if (command.equals(cmCloseAll)) {
            closeNonpermanentWindows();
            return true;
        }

        return super.onCommand(command);
    }

    /**
     * Method that TApplication subclasses can override to handle keystrokes.
     *
     * @param keypress keystroke event
     * @return if true, this event was consumed
     */
    @Override
    protected boolean onKeypress(final TKeypressEvent keypress) {
        // If compile status window is up, just close it now and bail out.
        // Otherwise we could have a modal or secondary thread window trying
        // to open on top of it, and weird shit can occur because it will try
        // to hide itself when it loses focus.
        if (compileStatusWindow.isShown()) {
            compileStatusWindow.hide();
            return true;
        }

        // Nothing to override yet.
        return super.onKeypress(keypress);
    }

    // ------------------------------------------------------------------------
    // UI behavior ------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Close non-permanent windows only.
     */
    private void closeNonpermanentWindows() {
        List<TWindow> closableWindows = getAllWindows();

        closableWindows.remove(compileStatusWindow);
        closableWindows.remove(messageWindow);
        closableWindows.remove(watchWindow);
        closableWindows.remove(localsWindow);
        closableWindows.remove(callstackWindow);
        if (projectWindow != null) {
            closableWindows.remove(projectWindow);
        }

        for (TWindow w: closableWindows) {
            closeWindow(w);
        }
    }

    /**
     * Display the about dialog.
     */
    @Override
    protected void showAboutDialog() {
        String version = getClass().getPackage().getImplementationVersion();
        if (version == null) {
            // This is Java 9+, use a hardcoded string here.
            version = VERSION;
        }
        messageBox(i18n.getString("aboutDialogTitle"),
            MessageFormat.format(i18n.getString("aboutDialogText"), version),
            TMessageBox.Type.OK);
    }

    /**
     * Add the menus for the UI.
     */
    private void addMenus() {

        // Tool menu
        TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
        toolMenu.addDefaultItem(TMenu.MID_REPAINT);
        toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
        toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
        TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
            getString("toolMenuStatus"));
        toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));

        // File menu
        TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
        fileMenu.addDefaultItem(TMenu.MID_NEW);
        fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
        fileMenu.addItem(MENU_FILE_SAVE,
            i18n.getString("fileMenuSave"), kbF2, false);
        fileMenu.addItem(MENU_FILE_SAVE_AS,
            i18n.getString("fileMenuSaveAs"), false);
        fileMenu.addItem(MENU_FILE_SAVE_ALL,
            i18n.getString("fileMenuSaveAll"), false);
        fileMenu.addSeparator();
        fileMenu.addDefaultItem(TMenu.MID_SHELL);
        fileMenu.addDefaultItem(TMenu.MID_EXIT);
        TStatusBar fileStatusBar = fileMenu.newStatusBar(i18n.
            getString("fileMenuStatus"));
        fileStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));

        // Edit menu
        TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
        editMenu.addDefaultItem(TMenu.MID_UNDO, false);
        editMenu.addDefaultItem(TMenu.MID_REDO, false);
        editMenu.addSeparator();
        editMenu.addDefaultItem(TMenu.MID_CUT, false);
        editMenu.addDefaultItem(TMenu.MID_COPY, false);
        editMenu.addDefaultItem(TMenu.MID_PASTE, false);
        editMenu.addDefaultItem(TMenu.MID_CLEAR, false);
        TStatusBar editStatusBar = editMenu.newStatusBar(i18n.
            getString("editMenuStatus"));
        editStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));

        // Search menu
        TMenu searchMenu = addMenu(i18n.getString("searchMenuTitle"));
        searchMenu.addDefaultItem(TMenu.MID_FIND, false);
        searchMenu.addDefaultItem(TMenu.MID_REPLACE, false);
        searchMenu.addDefaultItem(TMenu.MID_SEARCH_AGAIN, false);
        searchMenu.addSeparator();
        searchMenu.addDefaultItem(TMenu.MID_GOTO_LINE, false);
        searchMenu.addItem(MENU_SEARCH_PREVIOUS_ERROR,
            i18n.getString("searchMenuPreviousError"), kbAltF7);
        searchMenu.addItem(MENU_SEARCH_NEXT_ERROR,
            i18n.getString("searchMenuNextError"), kbAltF8);
        TStatusBar searchStatusBar = searchMenu.newStatusBar(i18n.
            getString("searchMenuStatus"));
        searchStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));

        // Run menu
        TMenu runMenu = addMenu(i18n.getString("runMenuTitle"));
        runMenu.addItem(MENU_RUN_RUN, i18n.getString("runMenuRun"), kbCtrlF9,
            true);
        runMenu.addItem(MENU_RUN_PROGRAM_RESET,
            i18n.getString("runMenuProgramReset"), kbCtrlF2, false);
        runMenu.addItem(MENU_RUN_GO_TO_CURSOR,
            i18n.getString("runMenuGoToCursor"), kbF4, true);
        runMenu.addItem(MENU_RUN_TRACE_INTO,
            i18n.getString("runMenuTraceInto"), kbF7, true);
        runMenu.addItem(MENU_RUN_STEP_OVER,
            i18n.getString("runMenuStepOver"), kbF8, true);
        runMenu.addItem(MENU_RUN_ARGUMENTS,
            i18n.getString("runMenuArguments"), false);
        TStatusBar runStatusBar = runMenu.newStatusBar(i18n.
            getString("runMenuStatus"));
        runStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));

        // Compile menu
        TMenu compileMenu = addMenu(i18n.getString("compileMenuTitle"));
        compileMenu.addItem(MENU_COMPILE_COMPILE,
            i18n.getString("compileMenuCompile"), kbAltF9);
        compileMenu.addItem(MENU_COMPILE_MAKE,
            i18n.getString("compileMenuMake"), kbF9, false);
        compileMenu.addItem(MENU_COMPILE_BUILD_ALL,
            i18n.getString("compileMenuBuildAll"), false);
        compileMenu.addSeparator();
        compileMenu.addItem(MENU_WINDOW_COMPILE,
            i18n.getString("windowCompileStatus"));
        compileMenu.addItem(MENU_COMPILE_REM_MESSAGES,
            i18n.getString("compileMenuRemoveMessages"));
        TStatusBar compileStatusBar = compileMenu.newStatusBar(i18n.
            getString("compileMenuStatus"));
        compileStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));

        // Debug menu
        TMenu debugMenu = addMenu(i18n.getString("debugMenuTitle"));
        debugMenu.addItem(MENU_DEBUG_LOCALS,
            i18n.getString("debugMenuLocals"), false);
        debugMenu.addItem(MENU_DEBUG_CALLSTACK,
            i18n.getString("debugMenuCallstack"), false);
        TSubMenu watchMenu = debugMenu.addSubMenu(i18n.getString(
            "debugMenuWatches"));
        debugMenu.addItem(MENU_DEBUG_TOGGLE_BREAKPOINT,
            i18n.getString("debugMenuToggleBreakpoint"), kbCtrlF8, false);
        debugMenu.addItem(MENU_DEBUG_BREAKPOINTS,
            i18n.getString("debugMenuBreakpoints"));
        TStatusBar debugStatusBar = debugMenu.newStatusBar(i18n.
            getString("debugMenuStatus"));
        debugStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));

        // Debug | Watches submenu
        watchMenu.addItem(MENU_WATCH_ADD,
            i18n.getString("watchMenuAdd"), kbCtrlF7, true);
        watchMenu.addItem(MENU_WATCH_DELETE,
            i18n.getString("watchMenuDelete"), true);
        watchMenu.addItem(MENU_WATCH_EDIT,
            i18n.getString("watchMenuEdit"), true);
        watchMenu.addItem(MENU_WATCH_REMOVE_ALL,
            i18n.getString("watchMenuRemoveAll"), true);

        // Project menu
        TMenu projectMenu = addMenu(i18n.getString("projectMenuTitle"));
        TStatusBar projectStatusBar = projectMenu.newStatusBar(i18n.
            getString("projectMenuStatus"));
        projectStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));
        projectMenu.addItem(MENU_PROJECT_NEW,
            i18n.getString("projectMenuNew"));
        projectMenu.addItem(MENU_PROJECT_OPEN,
            i18n.getString("projectMenuOpen"));
        projectMenu.addItem(MENU_PROJECT_CLOSE,
            i18n.getString("projectMenuClose"), false);
        projectMenu.addSeparator();
        projectMenu.addItem(MENU_PROJECT_ADD_ITEM,
            i18n.getString("projectMenuAddItem"), false);
        projectMenu.addItem(MENU_PROJECT_DELETE_ITEM,
            i18n.getString("projectMenuDeleteItem"), false);
        projectMenu.addItem(MENU_PROJECT_LOCAL_OPTIONS,
            i18n.getString("projectMenuLocalOptions"), false);

        // Options menu
        TMenu optionsMenu = addMenu(i18n.getString("optionsMenuTitle"));
        optionsMenu.addItem(MENU_OPTIONS_PROJECT,
            i18n.getString("optionsMenuProject"), false);
        optionsMenu.addItem(MENU_OPTIONS_APPLICATION,
            i18n.getString("optionsMenuApplication"));
        optionsMenu.addItem(MENU_OPTIONS_COLORS,
            i18n.getString("optionsMenuColors"));
        optionsMenu.addItem(MENU_OPTIONS_EDITOR,
            i18n.getString("optionsMenuEditor"));
        optionsMenu.addItem(MENU_OPTIONS_INTERFACE,
            i18n.getString("optionsMenuInterface"));
        optionsMenu.addSeparator();
        optionsMenu.addItem(MENU_OPTIONS_LOAD,
            i18n.getString("optionsMenuLoad"));
        optionsMenu.addItem(MENU_OPTIONS_SAVE,
            i18n.getString("optionsMenuSave"));
        TStatusBar optionsStatusBar = optionsMenu.newStatusBar(i18n.
            getString("optionsMenuStatus"));
        optionsStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));

        // Window menu
        TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle"));
        windowMenu.addDefaultItem(TMenu.MID_TILE);
        windowMenu.addDefaultItem(TMenu.MID_CASCADE);
        windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
        windowMenu.addSeparator();
        windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
        windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
        windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
        windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
        windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
        windowMenu.addSeparator();
        windowMenu.addItem(MENU_WINDOW_MESSAGE,
            i18n.getString("windowMenuMessage"));
        windowMenu.addItem(MENU_WINDOW_OUTPUT,
            i18n.getString("windowMenuOutput"), false);
        windowMenu.addItem(MENU_WINDOW_WATCH,
            i18n.getString("windowMenuWatch"));
        windowMenu.addItem(MENU_WINDOW_PROJECT,
            i18n.getString("windowMenuProject"));
        windowMenu.addItem(MENU_WINDOW_PROJECT_NOTES,
            i18n.getString("windowMenuProjectNotes"));
        windowMenu.addItem(MENU_WINDOW_COMPILE,
            i18n.getString("windowCompileStatus"));
        windowMenu.addSeparator();
        windowMenu.addItem(MENU_WINDOW_LIST_ALL,
            i18n.getString("windowMenuListAll"), kbAlt0);
        TStatusBar windowStatusBar = windowMenu.newStatusBar(i18n.
            getString("windowMenuStatus"));
        windowStatusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("Help"));

        // Help menu
        TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle"));
        helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
        helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
        helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
        helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
        helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
        helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
        helpMenu.addSeparator();
        helpMenu.addDefaultItem(TMenu.MID_ABOUT);
        TStatusBar helpStatusBar = helpMenu.newStatusBar(i18n.
            getString("helpMenuStatus"));
        helpStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));

    }

    /**
     * Add all the widgets of the UI.
     */
    private void addAllWidgets() {
        addMenus();

        compileStatusWindow = new CompileStatusWindow(this);
        messageWindow = new MessageWindow(this);
        watchWindow = new WatchWindow(this);
        localsWindow = new LocalsWindow(this);
        callstackWindow = new CallstackWindow(this);
    }

    /**
     * Get the CompileListener window.
     *
     * @return the compile listener window
     */
    public CompileListener getCompileListener() {
        return compileStatusWindow;
    }

    /**
     * Get the Message window.
     *
     * @return the message window
     */
    public MessageWindow getMessageWindow() {
        return messageWindow;
    }

    /**
     * Get the Watch window.
     *
     * @return the watch window
     */
    public WatchWindow getWatchWindow() {
        return watchWindow;
    }

    /**
     * Get the Locals window.
     *
     * @return the locals window
     */
    public LocalsWindow getLocalsWindow() {
        return localsWindow;
    }

    /**
     * Get the Callstack window.
     *
     * @return the callstack window
     */
    public CallstackWindow getCallstackWindow() {
        return callstackWindow;
    }

    /**
     * Get the Compile Status window.
     *
     * @return the compile status window
     */
    public CompileStatusWindow getCompileStatusWindow() {
        return compileStatusWindow;
    }

    /**
     * Get the Project window.
     *
     * @return the project window, or null if no project is open
     */
    public ProjectWindow getProjectWindow() {
        return projectWindow;
    }

    /**
     * Set the debugged program output window.
     *
     * @param window the output window
     */
    public void setOutputWindow(final TWindow window) {
        if (outputWindow != null) {
            closeWindow(outputWindow);
            outputWindow = null;
            disableMenuItem(MENU_WINDOW_OUTPUT);
        }
        if (window != null) {
            outputWindow = window;
            enableMenuItem(MENU_WINDOW_OUTPUT);
        }
    }

    // ------------------------------------------------------------------------
    // Options support --------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Initialize the options.
     */
    private void initializeOptions() {
        options = new Properties(getOptionDefaults());

        /*
         * See if tjide directory exists, and if not try to create it.
         */
        String homeDir = System.getProperty("user.home");
        String configDir = options.getProperty("tjide.configDir");
        File rcDir = null;

        if (configDir == null) {
            configFilename = homeDir + "/.tjide/tjide.properties";
            rcDir = new File(homeDir, ".tjide");
        } else {
            // configDir is set, either from the default tjide.properties, or
            // by another tjide.properties upstream in the classpath.
            rcDir = new File(configDir.replace("$HOME", homeDir));
            configFilename = rcDir.getPath() + "/tjide.properties";
        }

        if (rcDir.isFile()) {
            // A file exists where we expect ~/.tjide to be.  The user will
            // have to specify a new configFilename when they try to load or
            // save.
            configFilename = null;
        } else if (!rcDir.exists()) {
            // ~/.tjide needs to be created.
            rcDir.mkdir();
        }

        if (configFilename != null) {
            File configFile = new File(configFilename);
            if (!configFile.exists()) {
                // tjide.properties needs to be created.
                saveOptions();
            } else if (configFile.isDirectory()) {
                // A directory exists where we expect tjide.properties to be.
                // The user will have to specify a new configFilename when
                // they try to load or save.
                configFilename = null;
            }

            if (configFile.isFile()) {
                // tjide.properties is here, let's use it.
                loadOptions(configFilename);
            }
        }
    }

    /**
     * Get the default values for the supported options.
     *
     * @return the default values
     */
    private Properties getOptionDefaults() {
        // Load tjide.properties into the default properties.
        Properties defaults = new Properties();
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        try {
            defaults.load(loader.getResourceAsStream("tjide.properties"));
        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
        }
        return defaults;
    }

    /**
     * Load options from a file.
     *
     * @param filename the name of the file to read from
     */
    private void loadOptions(final String filename) {
        if (!(new File(filename)).isFile()) {
            return;
        }

        FileReader fileReader = null;
        try {
            fileReader = new FileReader(filename);
            options.load(fileReader);
        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    // SQUASH
                }
                fileReader = null;
            }
        }

        resolveOptions();
    }

    /**
     * Set default options.  Note package private access.
     */
    void setDefaultOptions() {

        // Application options, keep these in sync with tjide.properties.
        setOption("editor.useExternal", "false");

        setOption("editor.external.new", "$VISUAL");
        setOption("editor.external.open", "$VISUAL {0}");
        setOption("editor.external.openToLine", "$VISUAL +{1} {0}");

        setOption("editor.internal.undoLevel", "50");
        setOption("editor.internal.indentLevel", "4");
        setOption("editor.internal.backspaceUnindents", "true");
        setOption("editor.internal.highlightKeywords", "true");
        setOption("editor.internal.saveWithTabs", "false");
        setOption("editor.internal.trimWhitespace", "true");

        setOption("ui.font.name", "");
        setOption("ui.font.size", "20");
        setOption("ui.font.adjustX", "0");
        setOption("ui.font.adjustY", "0");
        setOption("ui.font.adjustWidth", "0");
        setOption("ui.font.adjustHeight", "0");

        setOption("compiler.java.useExternal", "false");
        setOption("compiler.java.jdkBin",
            "javac -Xpkginfo:always -g -sourcepath {0}/{1} -d {0}/{2} {0}/{3}");
        setOption("compiler.java.jreBin",
            "java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address={0} {1} -classpath {2}/{3}{4} {5} {6}");
        setOption("compiler.java.jreDebuggerAddress", "{0}:{1}");

        setOption("gjexer.TTerminal.ptypipe", "auto");
        setOption("gjexer.TTerminal.closeOnExit", "false");
        setOption("gjexer.Swing.cursorStyle", "underline");
        setOption("gjexer.Swing.tripleBuffer", "true");
        setOption("gjexer.ECMA48.rgbColor", "false");
        setOption("gjexer.ECMA48.sixel", "true");

        // Colors
        getTheme().setDefaultTheme();
        setTranquilColors();
        for (String key: getTheme().getColorNames()) {
            options.setProperty("colors." + key,
                getTheme().getColor(key).toString());
        }
    }

    /**
     * Save options to the user preferences file.  Note package private
     * access.
     */
    void saveOptions() {
        assert (configFilename != null);

        // Read the shipped tjide.properties, and replace the values with the
        // current option values.
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        BufferedReader reader = null;
        FileWriter writer = null;
        try {
            reader = new BufferedReader(new InputStreamReader(loader.
                    getResourceAsStream("tjide.properties")));
            writer = new FileWriter(configFilename);

            for (String line = reader.readLine(); line != null;
                 line = reader.readLine()) {

                if ((line.indexOf('=') == -1)
                    || (line.trim().startsWith("#"))
                ) {
                    // Comment or non-key-value line, write it and move on.
                    writer.write(line);
                    writer.write("\n");
                    continue;
                }

                // key-value line, replace value with actual.
                String key = line.substring(0, line.indexOf('=')).trim();
                String value = options.getProperty(key);
                writer.write(String.format("%s = %s\n", key, value));
            }

            // Save the colors.
            for (String key: getTheme().getColorNames()) {
                CellAttributes value = getTheme().getColor(key);
                writer.write(String.format("colors.%s = %s\n", key, value));
            }

        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // SQUASH
                }
                reader = null;
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // SQUASH
                }
                writer = null;
            }
        }
    }

    /**
     * Reset global variables to match loaded options.  Note package private
     * access.
     */
    void resolveOptions() {

        // Put some of the option values in other places.
        for (Object keyObj: options.keySet()) {
            String key = (String) keyObj;

            // colors.* is copied into the color theme.
            if (key.startsWith("colors.")) {
                String colorKey = key.substring(7);
                String colorValue = options.getProperty(key);

                /*
                System.err.println("colorKey '" + colorKey + "' colorValue '"
                    + colorValue + "'");
                */

                getTheme().setColorFromString(colorKey, colorValue);
            }

            // gjexer.* is copied into the main runtime properties.
            if (key.startsWith("gjexer.")) {
                System.setProperty(key, options.getProperty(key));
            }
        }
        // We may have changed some Jexer options, let the backend see those
        // changes.
        getBackend().reloadOptions();

        // Now reset any TJ variables based on option values.
        if (options.getProperty("editor.useExternal", "false").equals("true")) {
            useExternalEditor = true;
        } else {
            useExternalEditor = false;
        }
        if (getScreen() instanceof SwingTerminal) {
            SwingTerminal terminal = (SwingTerminal) getScreen();
            if (!options.getProperty("ui.font.name", "").equals("")) {
                try {
                    terminal.setFont(new Font(options.getProperty(
                            "ui.font.name"), Font.PLAIN,
                            Integer.parseInt(options.getProperty(
                                "ui.font.size"))));
                } catch (NumberFormatException e) {
                    // SQUASH
                }
            } else {
                try {
                    terminal.setFontSize(Integer.parseInt(
                        options.getProperty("ui.font.size")));
                } catch (NumberFormatException e) {
                    // SQUASH
                }
            }
            try {
                int adjustX = Integer.parseInt(options.getProperty(
                    "ui.font.adjustX"));
                if (adjustX != 0) {
                    terminal.setTextAdjustX(adjustX);
                }
            } catch (NumberFormatException e) {
                // SQUASH
            }
            try {
                int adjustY = Integer.parseInt(options.getProperty(
                    "ui.font.adjustY"));
                if (adjustY != 0) {
                    terminal.setTextAdjustY(adjustY);
                }
            } catch (NumberFormatException e) {
                // SQUASH
            }
            try {
                int adjustWidth = Integer.parseInt(options.getProperty(
                    "ui.font.adjustWidth"));
                if (adjustWidth != 0) {
                    terminal.setTextAdjustWidth(adjustWidth);
                }
            } catch (NumberFormatException e) {
                // SQUASH
            }
            try {
                int adjustHeight = Integer.parseInt(options.getProperty(
                    "ui.font.adjustHeight"));
                if (adjustHeight != 0) {
                    terminal.setTextAdjustHeight(adjustHeight);
                }
            } catch (NumberFormatException e) {
                // SQUASH
            }
        }
    }

    /**
     * Get an option value.
     *
     * @param key name of the option
     * @return the option value, or null if it is undefined
     */
    public String getOption(final String key) {
        return options.getProperty(key);
    }

    /**
     * Get an option value.
     *
     * @param key name of the option
     * @param defaultValue the value to return if the option is not defined
     * @return the option value, or defaultValue
     */
    public String getOption(final String key, final String defaultValue) {
        String result = options.getProperty(key);
        if (result == null) {
            return defaultValue;
        }
        return result;
    }

    /**
     * Set an option value.
     *
     * @param key name of the option
     * @param value the new the option value
     */
    public void setOption(final String key, final String value) {
        options.setProperty(key, value);
    }

    // ------------------------------------------------------------------------
    // Project support --------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Get the current project, or null if no project is open.
     *
     * @return the project, or null if no project is open
     */
    public Project getProject() {
        return project;
    }

    /**
     * Open a project.
     *
     * @param filename the name of the project file
     */
    public void openProject(final String filename) {
        closeProject();
        assert (project == null);

        try {
            project = new Project(this, filename);
            projectWindow = new ProjectWindow(this, project);

            watchWindow.loadAll(project.getWatches());

            enableMenuItem(MENU_COMPILE_MAKE);
            enableMenuItem(MENU_COMPILE_BUILD_ALL);
            enableMenuItem(MENU_PROJECT_CLOSE);
            enableMenuItem(MENU_PROJECT_ADD_ITEM);
            enableMenuItem(MENU_PROJECT_DELETE_ITEM);
            enableMenuItem(MENU_PROJECT_LOCAL_OPTIONS);
            enableMenuItem(MENU_OPTIONS_PROJECT);
            enableMenuItem(MENU_WINDOW_PROJECT);
            enableMenuItem(MENU_WINDOW_PROJECT_NOTES);

            // Switch to the top-most editor.
            for (TargetEditor editor: editingTargets.values()) {
                if (editor instanceof TWindow) {
                    TWindow window = (TWindow) editor;
                    if (window.getZ() == 1) {
                        window.activate();
                        break;
                    }
                }
            }

        } catch (Exception e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
        }
    }

    /**
     * Close the open project.
     */
    private void closeProject() {
        closeProject(true);
    }

    /**
     * Close the open project.  Note package private access.
     *
     * @param save if true, save the project
     */
    void closeProject(final boolean save) {
        closeProject(save, false);
    }

    /**
     * Close the open project.  Note package private access.
     *
     * @param save if true, save the project
     * @param inProjectWindowClose if true, this was called from
     * ProjectWindow.onClose()
     */
    void closeProject(final boolean save, final boolean inProjectWindowClose) {
        if (project != null) {

            // Close any open internal editors.  We leave the external
            // editors because they manage their own buffers/files.
            Set<FileTarget> openTargets = editingTargets.keySet();
            List<Target> projectTargets = project.getTargets();

            // Save the project first so that it can see the editor window
            // states, THEN close the editor windows.
            if (save == true) {
                project.save();
            }
            List<TWindow> windowsToClose = new ArrayList<TWindow>();
            for (FileTarget target: openTargets) {
                if ((target.getWindow() != null)
                    && projectTargets.contains(target)
                ) {
                    TargetEditor editor = editingTargets.get(target);
                    assert (target.getWindow() == editor);
                    if (editor instanceof InternalEditorWindow) {
                        windowsToClose.add((InternalEditorWindow) editor);
                    }
                }
            }
            for (TWindow window: windowsToClose) {
                closeWindow(window);
            }
            project = null;

            if (inProjectWindowClose == false) {
                projectWindow.close();
                projectWindow = null;
            }
        }
        disableMenuItem(MENU_COMPILE_MAKE);
        disableMenuItem(MENU_COMPILE_BUILD_ALL);
        disableMenuItem(MENU_PROJECT_CLOSE);
        disableMenuItem(MENU_PROJECT_ADD_ITEM);
        disableMenuItem(MENU_PROJECT_DELETE_ITEM);
        disableMenuItem(MENU_PROJECT_LOCAL_OPTIONS);
        disableMenuItem(MENU_OPTIONS_PROJECT);
        disableMenuItem(MENU_WINDOW_PROJECT);
        disableMenuItem(MENU_WINDOW_PROJECT_NOTES);

        watchWindow.clearAll();
    }

    /**
     * Convenience function to open a file in an editor window and make it
     * active.
     *
     * @param project the project metadata
     * @param fileTarget the file target to open
     * @return the editor window opened, either a TTerminalWindow or a
     * TEditorWindow, or null if filename exists and is not a file
     * @throws IOException if a java.io operation throws
     */
    public TWindow openEditor(final Project project,
        final FileTarget fileTarget) throws IOException {

        String filename = (new File(project.getFullSourceDir(),
                fileTarget.getName())).getPath();
        return openEditor(filename, fileTarget);
    }

    /**
     * Convenience function to open a file in an internal editor window and
     * make it active.
     *
     * @param project the project metadata
     * @param fileTarget the file target to open
     * @return the editor window opened, either a TTerminalWindow or a
     * TEditorWindow, or null if filename exists and is not a file
     * @throws IOException if a java.io operation throws
     */
    public InternalEditorWindow openInternalEditor(final Project project,
        final FileTarget fileTarget) throws IOException {

        String filename = (new File(new File(project.getRootDir(),
                    project.getSourceDir()),
                fileTarget.getName())).getPath();

        return openInternalEditor(filename, fileTarget);
    }

    /**
     * Convenience function to open a file in an editor window and make it
     * active.
     *
     * @param filename the file to open
     * @return the editor window opened, either a TTerminalWindow or a
     * TEditorWindow, or null if filename exists and is not a file
     * @throws IOException if a java.io operation throws
     */
    public TWindow openEditor(final String filename) throws IOException {
        return openEditor(filename, null);
    }

    /**
     * Convenience function to open a file in an editor window and make it
     * active.
     *
     * @param filename the file to open
     * @param target the project target
     * @return the editor window opened, either a TTerminalWindow or a
     * TEditorWindow, or null if filename exists and is not a file
     * @throws IOException if a java.io operation throws
     */
    public TWindow openEditor(final String filename,
        final FileTarget target) throws IOException {

        File file = new File(filename);
        if (file.exists() && !file.isFile()) {
            return null;
        }

        if (useExternalEditor) {
            ExternalEditorWindow editor;
            editor = new ExternalEditorWindow(this, filename);
            editingTargets.put(target, editor);
            if (target != null) {
                target.setWindow(editor);
                editor.setTarget(target);
            }
            return editor;
        } else {
            return openInternalEditor(filename, target);
        }
    }

    /**
     * Convenience function to open a file in an internal editor window and
     * make it active.
     *
     * @param filename the file to open
     * @param target the project target
     * @return the editor window opened, either a TTerminalWindow or a
     * TEditorWindow, or null if filename exists and is not a file
     * @throws IOException if a java.io operation throws
     */
    public InternalEditorWindow openInternalEditor(final String filename,
        final FileTarget target) throws IOException {

        File file = new File(filename);
        if (file.exists() && !file.isFile()) {
            return null;
        }

        InternalEditorWindow editor;
        try {
            editor = new InternalEditorWindow(this, new File(filename),
                target, 0, 0, getScreen().getWidth(),
                getScreen().getHeight() - messageWindow.getHeight());
        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(this, e);
            return null;
        }
        editingTargets.put(target, editor);
        enableMenuItem(MENU_FILE_SAVE_ALL);
        if (target != null) {
            target.setWindow(editor);
        }
        return editor;
    }

    /**
     * Retrieve the internal editor window associated with a particular
     * target.
     *
     * @param target the target
     * @return the editor, or null if there is no internal editor associated
     * with target (but there might an external editor associated with the
     * target)
     */
    public InternalEditorWindow findInternalEditor(final FileTarget target) {
        TargetEditor editor = editingTargets.get(target);
        assert (target.getWindow() == editor);
        if (editor instanceof InternalEditorWindow) {
            return (InternalEditorWindow) editor;
        }
        return null;
    }

    /**
     * Remove the editor window association with a particular target.  Note
     * package private access.
     *
     * @param target the target
     */
    void removeEditor(final FileTarget target) {

        if (target != null) {
            // This is defensive.  InternalEditor should have already unset
            // target.
            target.setWindow(null);
            editingTargets.remove(target);
        }

        // If there are no internal editors left, disable File | Save all.
        boolean hasInternalEditor = false;
        for (TargetEditor editor: editingTargets.values()) {
            if (editor instanceof InternalEditorWindow) {
                hasInternalEditor = true;
                break;
            }
        }
        if (!hasInternalEditor) {
            disableMenuItem(MENU_FILE_SAVE_ALL);
        }
    }

    /**
     * Either switch to, or open a new editor window for a particular target.
     *
     * @param project the project metadata
     * @param target the target
     * @throws IOException if a java.io operation throws
     */
    public void findTargetEditor(final Project project,
        final FileTarget target) throws IOException {

        TargetEditor editor = editingTargets.get(target);

        if (editor != null) {
            assert (target.getWindow() == editor);
            ((TWindow) editor).activate();
            return;
        }
        TWindow editorWindow = openEditor(project, target);
        assert (editorWindow != null);
        editorWindow.activate();
    }

    /**
     * Manually add an editor to the editing targets list.  Note package
     * private access.
     *
     * @param target the target to add
     * @param editor the editor to associate target with
     */
    void addEditingTarget(final FileTarget target,
        final InternalEditorWindow editor) {

        editingTargets.put(target, editor);
        enableMenuItem(MENU_FILE_SAVE_ALL);
    }

    /**
     * Retrieve all of the open internal editor windows.
     *
     * @return a list of editors, which may be empty
     */
    public List<InternalEditorWindow> getInternalEditors() {
        List<InternalEditorWindow> editors = new ArrayList<InternalEditorWindow>();

        for (TargetEditor editor: editingTargets.values()) {
            if (editor instanceof InternalEditorWindow) {
                editors.add((InternalEditorWindow) editor);
            }
        }
        return editors;
    }

    /**
     * Get the debugger callback interface.
     *
     * @return the debugger, or null if there is no active debug session
     */
    public Debugger getDebugger() {
        return debugger;
    }

    // ------------------------------------------------------------------------
    // DebuggerListener -------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Notify of debugged program exit.
     *
     * @param exitCode the return code of the debugged program
     */
    public void setDebugProgramExited(final int exitCode) {
        /*
         * We collect the exit code, but do not have a spot to expose it.
         */
        invokeLater(new Runnable() {
            public void run() {
                menuRunProgramReset();
            }
        });
    }

    /**
     * Save the Scope at a specific execution location.
     *
     * @param debugScope the scope to save
     */
    public void setDebugScope(final Scope debugScope) {

        /*
        System.err.println("Execution is at '" +
            debugScope.getSourceFilename() + "' " + debugScope.getLine());
        */

        invokeLater(new Runnable() {
            public void run() {
                // Save debug location
                TranquilApplication.this.debugScope = debugScope;

                // Set editor to match execution line.
                boolean found = false;
                for (TargetEditor targetEditor: editingTargets.values()) {
                    if (targetEditor instanceof InternalEditorWindow) {
                        InternalEditorWindow editor;
                        editor = (InternalEditorWindow) targetEditor;
                        FileTarget target = editor.getTarget();
                        if (target instanceof JavaTarget) {
                            JavaTarget javaTarget = (JavaTarget) target;
                            if (javaTarget.getName().equals(debugScope.
                                    getSourceFilename())
                            ) {
                                int line = debugScope.getLine();
                                editor.setExecutionLine(line);
                                editor.setEditingRowNumber(line);
                                found = true;
                            } else {
                                editor.setExecutionLine(-1);
                            }
                        } else {
                            editor.setExecutionLine(-1);
                        }
                    }
                }
                if (!found) {
                    if (project == null) {
                        // The user has a breakpoint on an external editor,
                        // with no project to track back to.  Just bail out.
                        return;
                    }

                    for (Target target: project.getTargets()) {
                        if (target instanceof JavaTarget) {
                            JavaTarget javaTarget = (JavaTarget) target;
                            if (javaTarget.getName().equals(debugScope.
                                    getSourceFilename())
                            ) {
                                InternalEditorWindow editor = null;
                                try {
                                    editor = openInternalEditor(project,
                                        javaTarget);
                                } catch (IOException e) {
                                    new TExceptionDialog(TranquilApplication.this, e);
                                }
                                if (editor != null) {
                                    int line = debugScope.getLine();
                                    editor.setExecutionLine(line);
                                    editor.setEditingRowNumber(line);
                                }
                            }
                        }
                    }
                }

                // Update debug windows
                watchWindow.updateWatches();
                localsWindow.updateLocals();
                callstackWindow.updateCallstack();
            }
        });
    }

    /**
     * Get the last seen debug scope.
     *
     * @return the debug scope
     */
    public Scope getDebugScope() {
        return debugScope;
    }

    /**
     * Set the debugger callback interface, so that the DebuggerListener can
     * control the program.
     *
     * @param debugger the debugger
     */
    public void setDebugger(final Debugger debugger) {
        if (this.debugger != null) {
            this.debugger.reset();
            this.debugger = null;
        }
        this.debugger = debugger;
        debugScope = null;
        enableMenuItem(MENU_RUN_PROGRAM_RESET);
        enableMenuItem(MENU_DEBUG_CALLSTACK);
        enableMenuItem(MENU_DEBUG_LOCALS);
     }

}
